package org.gw.ylc.base.data;

/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */

import antlr.*;
import antlr.collections.AST;
import org.hibernate.QueryException;
import org.hibernate.hql.internal.antlr.HqlBaseLexer;
import org.hibernate.hql.internal.antlr.HqlBaseParser;
import org.hibernate.hql.internal.antlr.HqlTokenTypes;
import org.hibernate.hql.internal.ast.ErrorCounter;
import org.hibernate.hql.internal.ast.HqlASTFactory;
import org.hibernate.hql.internal.ast.HqlToken;
import org.hibernate.hql.internal.ast.ParseErrorHandler;
import org.hibernate.hql.internal.ast.util.ASTPrinter;
import org.hibernate.hql.internal.ast.util.ASTUtil;
import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.StringHelper;

import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringReader;
import java.util.*;

/**
 * Implements the semantic action methods defined in the HQL base parser to keep
 * the grammar source file a little cleaner. Extends the parser class generated
 * by ANTLR.
 *
 * @author Joshua Davis (pgmjsd@sourceforge.net)
 */
public final class FilteredHQlParser extends HqlBaseParser {
	private static final CoreMessageLogger LOG = CoreLogging.messageLogger(FilteredHQlParser.class);

	private final ParseErrorHandler parseErrorHandler;
	private final ASTPrinter printer = getASTPrinter();

	private static ASTPrinter getASTPrinter() {
		return new ASTPrinter(HqlTokenTypes.class);
	}

	/**
	 * Get a HqlParser instance for the given HQL string.
	 *
	 * @param hql
	 *            The HQL query string
	 *
	 * @return The parser.
	 */
	public static FilteredHQlParser getInstance(String hql) {
		return new FilteredHQlParser(hql);
	}

	public FilteredHQlParser(String hql) {
		// The fix for HHH-558...
		super(new HqlBaseLexer(new StringReader(hql)));
		parseErrorHandler = new ErrorCounter(hql);
		// Create nodes that track line and column number.
		setASTFactory(new HqlASTFactory());
	}

	// handle trace logging
	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

	private int traceDepth;

	@Override
	public void traceIn(String ruleName) {
		if (!LOG.isTraceEnabled()) {
			return;
		}
		if (inputState.guessing > 0) {
			return;
		}
		String prefix = StringHelper.repeat('-', (traceDepth++ * 2)) + "-> ";
		LOG.trace(prefix + ruleName);
	}

	@Override
	public void traceOut(String ruleName) {
		if (!LOG.isTraceEnabled()) {
			return;
		}
		if (inputState.guessing > 0) {
			return;
		}
		String prefix = "<-" + StringHelper.repeat('-', (--traceDepth * 2)) + " ";
		LOG.trace(prefix + ruleName);
	}

	@Override
	public void reportError(RecognitionException e) {
		parseErrorHandler.reportError(e); // Use the delegate.
	}

	@Override
	public void reportError(String s) {
		parseErrorHandler.reportError(s); // Use the delegate.
	}

	@Override
	public void reportWarning(String s) {
		parseErrorHandler.reportWarning(s);
	}

	public ParseErrorHandler getParseErrorHandler() {
		return parseErrorHandler;
	}

	/**
	 * Overrides the base behavior to retry keywords as identifiers.
	 *
	 * @param token
	 *            The token.
	 * @param ex
	 *            The recognition exception.
	 *
	 * @return AST - The new AST.
	 *
	 * @throws RecognitionException
	 *             if the substitution was not possible.
	 * @throws TokenStreamException
	 *             if the substitution was not possible.
	 */
	@Override
	public AST handleIdentifierError(Token token, RecognitionException ex)
			throws RecognitionException, TokenStreamException {
		// If the token can tell us if it could be an identifier...
		if (token instanceof HqlToken) {
			HqlToken hqlToken = (HqlToken) token;
			// ... and the token could be an identifier and the error is
			// a mismatched token error ...
			if (hqlToken.isPossibleID() && (ex instanceof MismatchedTokenException)) {
				MismatchedTokenException mte = (MismatchedTokenException) ex;
				// ... and the expected token type was an identifier, then:
				if (mte.expecting == HqlTokenTypes.IDENT) {
					// Use the token as an identifier.
					reportWarning("Keyword  '" + token.getText() + "' is being interpreted as an identifier due to: "
							+ mte.getMessage());
					// Add the token to the AST.
					ASTPair currentAST = new ASTPair();
					token.setType(HqlTokenTypes.WEIRD_IDENT);
					astFactory.addASTChild(currentAST, astFactory.create(token));
					consume();
					return currentAST.root;
				}
			} // if
		} // if
			// Otherwise, handle the error normally.
		return super.handleIdentifierError(token, ex);
	}

	/**
	 * Returns an equivalent tree for (NOT (a relop b) ), for example:
	 * 
	 * <pre>
	 * (NOT (GT a b) ) => (LE a b)
	 * </pre>
	 *
	 * @param x
	 *            The sub tree to transform, the parent is assumed to be NOT.
	 *
	 * @return AST - The equivalent sub-tree.
	 */
	@Override
	public AST negateNode(AST x) {
		// TODO: switch statements are always evil! We already had bugs because
		// of forgotten token types. Use polymorphism for this!
		switch (x.getType()) {
		case OR: {
			x.setType(AND);
			x.setText("{and}");
			x.setFirstChild(negateNode(x.getFirstChild()));
			x.getFirstChild().setNextSibling(negateNode(x.getFirstChild().getNextSibling()));
			return x;
		}
		case AND: {
			x.setType(OR);
			x.setText("{or}");
			x.setFirstChild(negateNode(x.getFirstChild()));
			x.getFirstChild().setNextSibling(negateNode(x.getFirstChild().getNextSibling()));
			return x;
		}
		case EQ: {
			// (NOT (EQ a b) ) => (NE a b)
			x.setType(NE);
			x.setText("{not}" + x.getText());
			return x;
		}
		case NE: {
			// (NOT (NE a b) ) => (EQ a b)
			x.setType(EQ);
			x.setText("{not}" + x.getText());
			return x;
		}
		case GT: {
			// (NOT (GT a b) ) => (LE a b)
			x.setType(LE);
			x.setText("{not}" + x.getText());
			return x;
		}
		case LT: {
			// (NOT (LT a b) ) => (GE a b)
			x.setType(GE);
			x.setText("{not}" + x.getText());
			return x;
		}
		case GE: {
			// (NOT (GE a b) ) => (LT a b)
			x.setType(LT);
			x.setText("{not}" + x.getText());
			return x;
		}
		case LE: {
			// (NOT (LE a b) ) => (GT a b)
			x.setType(GT);
			x.setText("{not}" + x.getText());
			return x;
		}
		case LIKE: {
			// (NOT (LIKE a b) ) => (NOT_LIKE a b)
			x.setType(NOT_LIKE);
			x.setText("{not}" + x.getText());
			return x;
		}
		case NOT_LIKE: {
			// (NOT (NOT_LIKE a b) ) => (LIKE a b)
			x.setType(LIKE);
			x.setText("{not}" + x.getText());
			return x;
		}
		case IN: {
			x.setType(NOT_IN);
			x.setText("{not}" + x.getText());
			return x;
		}
		case NOT_IN: {
			x.setType(IN);
			x.setText("{not}" + x.getText());
			return x;
		}
		case IS_NULL: {
			// (NOT (IS_NULL a b) ) => (IS_NOT_NULL a b)
			x.setType(IS_NOT_NULL);
			x.setText("{not}" + x.getText());
			return x;
		}
		case IS_NOT_NULL: {
			// (NOT (IS_NOT_NULL a b) ) => (IS_NULL a b)
			x.setType(IS_NULL);
			x.setText("{not}" + x.getText());
			return x;
		}
		case BETWEEN: {
			// (NOT (BETWEEN a b) ) => (NOT_BETWEEN a b)
			x.setType(NOT_BETWEEN);
			x.setText("{not}" + x.getText());
			return x;
		}
		case NOT_BETWEEN: {
			// (NOT (NOT_BETWEEN a b) ) => (BETWEEN a b)
			x.setType(BETWEEN);
			x.setText("{not}" + x.getText());
			return x;
		}
		/*
		 * This can never happen because this rule will always eliminate the
		 * child NOT. case NOT: { // (NOT (NOT x) ) => (x) return
		 * x.getFirstChild(); }
		 */
		default: {
			// Just add a 'not' parent.
			AST not = super.negateNode(x);
			if (not != x) {
				// relink the next sibling to the new 'not' parent
				not.setNextSibling(x.getNextSibling());
				x.setNextSibling(null);
			}
			return not;
		}
		}
	}

	/**
	 * Post process equality expressions, clean up the subtree.
	 *
	 * @param x
	 *            The equality expression.
	 *
	 * @return AST - The clean sub-tree.
	 */
	@Override
	public AST processEqualityExpression(AST x) {
		if (x == null) {
			LOG.processEqualityExpression();
			return null;
		}

		int type = x.getType();
		if (type == EQ || type == NE) {
			boolean negated = type == NE;
			if (x.getNumberOfChildren() == 2) {
				AST a = x.getFirstChild();
				AST b = a.getNextSibling();
				// (EQ NULL b) => (IS_NULL b)
				if (a.getType() == NULL && b.getType() != NULL) {
					return createIsNullParent(b, negated);
				}
				// (EQ a NULL) => (IS_NULL a)
				else if (b.getType() == NULL && a.getType() != NULL) {
					return createIsNullParent(a, negated);
				} else if (b.getType() == EMPTY) {
					return processIsEmpty(a, negated);
				} else {
					return x;
				}
			} else {
				return x;
			}
		} else {
			return x;
		}
	}

	private AST createIsNullParent(AST node, boolean negated) {
		node.setNextSibling(null);
		int type = negated ? IS_NOT_NULL : IS_NULL;
		String text = negated ? "is not null" : "is null";
		return ASTUtil.createParent(astFactory, type, text, node);
	}

	private AST processIsEmpty(AST node, boolean negated) {
		node.setNextSibling(null);
		// NOTE: Because we're using ASTUtil.createParent(), the tree must be
		// created from the bottom up.
		// IS EMPTY x => (EXISTS (QUERY (SELECT_FROM (FROM x) ) ) )
		AST ast = createSubquery(node);
		ast = ASTUtil.createParent(astFactory, EXISTS, "exists", ast);
		// Add NOT if it's negated.
		if (!negated) {
			ast = ASTUtil.createParent(astFactory, NOT, "not", ast);
		}
		return ast;
	}

	private AST createSubquery(AST node) {
		AST ast = ASTUtil.createParent(astFactory, RANGE, "RANGE", node);
		ast = ASTUtil.createParent(astFactory, FROM, "from", ast);
		ast = ASTUtil.createParent(astFactory, SELECT_FROM, "SELECT_FROM", ast);
		ast = ASTUtil.createParent(astFactory, QUERY, "QUERY", ast);
		return ast;
	}

	public void showAst(AST ast, PrintStream out) {
		showAst(ast, new PrintWriter(out));
	}

	private void showAst(AST ast, PrintWriter pw) {
		printer.showAst(ast, pw);
	}

	@Override
	public void weakKeywords() throws TokenStreamException {

		int t = LA(1);
		switch (t) {
		case ORDER:
		case GROUP:
			// Case 1: Multi token keywords GROUP BY and ORDER BY
			// The next token ( LT(2) ) should be 'by'... otherwise, this is
			// just an ident.
			if (LA(2) != LITERAL_by) {
				LT(1).setType(IDENT);
				if (LOG.isDebugEnabled()) {
					LOG.debugf("weakKeywords() : new LT(1) token - %s", LT(1));
				}
			}
			break;
		default:
			// Case 2: The current token is after FROM and before '.'.
			if (LA(0) == FROM && t != IDENT && LA(2) == DOT) {
				HqlToken hqlToken = (HqlToken) LT(1);
				if (hqlToken.isPossibleID()) {
					hqlToken.setType(IDENT);
					if (LOG.isDebugEnabled()) {
						LOG.debugf("weakKeywords() : new LT(1) token - %s", LT(1));
					}
				}
			}
			break;
		}
	}

	@Override
	public void expectNamedParameterName() throws TokenStreamException {
		// we expect the token following a COLON (':') to be the name of a named
		// parameter.
		// if the following token is anything other than IDENT we convert its
		// type if possible.

		// NOTE : the LT() call is more expensive than the LA() call; so we
		// use LA() first to see if LT() is needed.
		if (LA(1) != IDENT) {
			final HqlToken nextToken = (HqlToken) LT(1);
			if (nextToken.isPossibleID()) {
				LOG.debugf("Converting keyword [%s] following COLON to IDENT as an expected parameter name",
						nextToken.getText());
				nextToken.setType(IDENT);
			}
		}
	}

	@Override
	public void handleDotIdent() throws TokenStreamException {
		// This handles HHH-354, where there is a strange property name in a
		// where clause.
		// If the lookahead contains a DOT then something that isn't an IDENT...
		if (LA(1) == DOT && LA(2) != IDENT) {
			// See if the second lookahead token can be an identifier.
			HqlToken t = (HqlToken) LT(2);
			if (t.isPossibleID()) {
				// Set it!
				LT(2).setType(IDENT);
				if (LOG.isDebugEnabled()) {
					LOG.debugf("handleDotIdent() : new LT(2) token - %s", LT(1));
				}
			}
		}
	}

	@Override
	public void processMemberOf(Token n, AST p, ASTPair currentAST) {
		// convert MEMBER OF to the equivalent IN ELEMENTS structure...
		AST inNode = n == null ? astFactory.create(IN, "in") : astFactory.create(NOT_IN, "not in");
		astFactory.makeASTRoot(currentAST, inNode);

		AST inListNode = astFactory.create(IN_LIST, "inList");
		inNode.addChild(inListNode);
		AST elementsNode = astFactory.create(ELEMENTS, "elements");
		inListNode.addChild(elementsNode);
		elementsNode.addChild(p);
	}

	private Map<String, Set<String>> treatMap;

	@Override
	protected void registerTreat(AST pathToTreat, AST treatAs) {
		final String path = toPathText(pathToTreat);
		final String subclassName = toPathText(treatAs);
		LOG.debugf("Registering discovered request to treat(%s as %s)", path, subclassName);

		if (treatMap == null) {
			treatMap = new HashMap<String, Set<String>>();
		}

		Set<String> subclassNames = treatMap.get(path);
		if (subclassNames == null) {
			subclassNames = new HashSet<String>();
			treatMap.put(path, subclassNames);
		}
		subclassNames.add(subclassName);
	}

	private String toPathText(AST node) {
		final String text = node.getText();
		if (text.equals(".") && node.getFirstChild() != null && node.getFirstChild().getNextSibling() != null
				&& node.getFirstChild().getNextSibling().getNextSibling() == null) {
			return toPathText(node.getFirstChild()) + '.' + toPathText(node.getFirstChild().getNextSibling());
		}
		return text;
	}

	public Map<String, Set<String>> getTreatMap() {
		return treatMap == null ? Collections.<String, Set<String>> emptyMap() : treatMap;
	}

	public static void panic() {
		// overriden to avoid System.exit
		throw new QueryException("Parser: panic");
	}
}
